agentmux_srv\backend\wconfig/
loader.rs1use std::collections::HashMap;
7use std::path::PathBuf;
8
9use super::types::{ConfigError, FullConfigType, WidgetConfigType};
10
11pub fn build_default_config() -> FullConfigType {
18 let mut config = FullConfigType::default();
19
20 const WIDGETS_JSON: &str =
22 include_str!("../../config/widgets.json");
23
24 match serde_json::from_str::<HashMap<String, WidgetConfigType>>(WIDGETS_JSON) {
25 Ok(widgets) => {
26 config.widgets = widgets;
27 }
28 Err(e) => {
29 eprintln!("wconfig: failed to parse embedded widgets.json: {}", e);
30 }
31 }
32
33 config
34}
35
36pub fn read_config_file<T: serde::de::DeserializeOwned + Default>(
40 path: &PathBuf,
41) -> (T, Vec<ConfigError>) {
42 let mut errors = Vec::new();
43
44 let content = match std::fs::read_to_string(path) {
45 Ok(c) => c,
46 Err(e) if e.kind() == std::io::ErrorKind::NotFound => return (T::default(), errors),
47 Err(e) => {
48 errors.push(ConfigError {
49 file: path.to_string_lossy().to_string(),
50 err: format!("cannot read file: {}", e),
51 });
52 return (T::default(), errors);
53 }
54 };
55
56 let stripped = json_comments::StripComments::new(content.as_bytes());
58 let mut json_bytes = Vec::new();
59 std::io::Read::read_to_end(&mut std::io::BufReader::new(stripped), &mut json_bytes)
60 .unwrap_or_default();
61
62 let json_str = strip_trailing_commas(&String::from_utf8_lossy(&json_bytes));
64 let clean: Result<T, _> = serde_json::from_str(&json_str);
65
66 match clean {
67 Ok(parsed) => (parsed, errors),
68 Err(e) => {
69 errors.push(ConfigError {
70 file: path.to_string_lossy().to_string(),
71 err: format!("JSON parse error: {}", e),
72 });
73 (T::default(), errors)
74 }
75 }
76}
77
78pub fn read_settings_raw_jsonc(path: &std::path::Path) -> serde_json::Map<String, serde_json::Value> {
81 if !path.exists() {
82 return serde_json::Map::new();
83 }
84 match std::fs::read_to_string(path) {
85 Ok(content) => parse_jsonc_to_map(&content),
86 Err(_) => serde_json::Map::new(),
87 }
88}
89
90pub fn parse_jsonc_to_map(content: &str) -> serde_json::Map<String, serde_json::Value> {
92 let stripped_comments = json_comments::StripComments::new(content.as_bytes());
93 let mut json_bytes = Vec::new();
94 std::io::Read::read_to_end(&mut std::io::BufReader::new(stripped_comments), &mut json_bytes)
95 .unwrap_or_default();
96 let json_str = strip_trailing_commas(&String::from_utf8_lossy(&json_bytes));
97 match serde_json::from_str::<serde_json::Value>(&json_str) {
98 Ok(serde_json::Value::Object(map)) => map,
99 _ => serde_json::Map::new(),
100 }
101}
102
103pub fn merge_into_template(
112 template: &str,
113 user_settings: &serde_json::Map<String, serde_json::Value>,
114) -> String {
115 if user_settings.is_empty() {
116 return template.to_string();
117 }
118
119 let mut remaining: std::collections::HashMap<&str, &serde_json::Value> =
120 user_settings.iter().map(|(k, v)| (k.as_str(), v)).collect();
121 let mut lines: Vec<String> = Vec::new();
122
123 for line in template.lines() {
124 if let Some(key) = extract_commented_setting_key(line) {
125 if let Some(value) = remaining.remove(key) {
126 let indent: String = line.chars().take_while(|c| c.is_whitespace()).collect();
128 let val_str = serde_json::to_string(value).unwrap_or_default();
129 lines.push(format!("{}\"{}\": {},", indent, key, val_str));
130 continue;
131 }
132 }
133 lines.push(line.to_string());
134 }
135
136 if !remaining.is_empty() {
138 if let Some(brace_pos) = lines.iter().rposition(|l| l.trim() == "}") {
140 let mut extra: Vec<String> = Vec::new();
141 extra.push(String::new());
142 extra.push(" // -- User Overrides --".to_string());
143 let mut sorted_keys: Vec<&&str> = remaining.keys().collect();
144 sorted_keys.sort();
145 for key in sorted_keys {
146 let value = remaining[*key];
147 let val_str = serde_json::to_string(value).unwrap_or_default();
148 extra.push(format!(" \"{}\": {},", key, val_str));
149 }
150 for (i, line) in extra.into_iter().enumerate() {
151 lines.insert(brace_pos + i, line);
152 }
153 }
154 }
155
156 let mut result = lines.join("\n");
157 if !result.ends_with('\n') {
159 result.push('\n');
160 }
161 result
162}
163
164fn extract_commented_setting_key(line: &str) -> Option<&str> {
168 let trimmed = line.trim_start();
169 let rest = trimmed.strip_prefix("//")?;
170 let rest = rest.trim_start();
171 let rest = rest.strip_prefix('"')?;
172 let end = rest.find('"')?;
173 Some(&rest[..end])
174}
175
176pub(super) fn strip_trailing_commas(input: &str) -> String {
177 let mut result = String::with_capacity(input.len());
178 let mut chars = input.chars().peekable();
179 let mut in_string = false;
180 let mut last_comma_pos: Option<usize> = None;
181
182 while let Some(ch) = chars.next() {
183 if in_string {
184 result.push(ch);
185 if ch == '\\' {
186 if let Some(&next) = chars.peek() {
187 result.push(next);
188 chars.next();
189 }
190 } else if ch == '"' {
191 in_string = false;
192 }
193 } else {
194 match ch {
195 '"' => {
196 in_string = true;
197 last_comma_pos = None;
198 result.push(ch);
199 }
200 ',' => {
201 last_comma_pos = Some(result.len());
202 result.push(ch);
203 }
204 '}' | ']' => {
205 if let Some(pos) = last_comma_pos {
206 result.replace_range(pos..pos + 1, " ");
207 }
208 last_comma_pos = None;
209 result.push(ch);
210 }
211 c if c.is_whitespace() => {
212 result.push(ch);
213 }
214 _ => {
215 last_comma_pos = None;
216 result.push(ch);
217 }
218 }
219 }
220 }
221 result
222}
223
224#[allow(dead_code)]
226pub fn expand_env_vars(s: &str) -> String {
227 let mut result = s.to_string();
228 let mut start = 0;
229
230 while let Some(idx) = result[start..].find("$ENV:") {
231 let abs_idx = start + idx;
232 let rest = &result[abs_idx + 5..];
233
234 let end = rest
236 .find(|c: char| !c.is_ascii_alphanumeric() && c != '_' && c != ':')
237 .unwrap_or(rest.len());
238
239 let var_spec = &rest[..end];
240
241 let (var_name, fallback) = if let Some(colon_idx) = var_spec.find(':') {
243 (&var_spec[..colon_idx], Some(&var_spec[colon_idx + 1..]))
244 } else {
245 (var_spec, None)
246 };
247
248 let value = std::env::var(var_name).unwrap_or_else(|_| {
249 fallback.unwrap_or("").to_string()
250 });
251
252 let full_pattern = format!("$ENV:{}", var_spec);
253 result = result.replacen(&full_pattern, &value, 1);
254 start = abs_idx + value.len();
255 }
256
257 result
258}